<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div id="app">
        <h1>插值表达式</h1>
        <h3>{{msg}}</h3>
        <h3>{{count}}</h3>
        <h1>v-text</h1>
        <div v-text='msg'></div>
        <h1>v-model</h1>
        <input type="text" v-model='msg' name="" id="">
        <input type="text" v-model='count' name="" id="">
        <div>{{msg}}</div>
    </div>
</body>

</html>
<script>

    // 具体实现步骤
    // 1： 通过属性  保存选项的数据
    // 2： 把data中的成员 转换为getter和setter  注入到vue实例中 方便使用
    // 3：调用observer对象 监听数据变化
    // 4：调用compiler 解析指令和插值表达式
    class Vue {
        constructor(options) {
            // 通过属性  保存选项的数据
            this.$options = options || {};//如果我们在调用vue构造函数的时候 没有传入参数 我们初始化一个空对象
            this.$data = options.data || {};
            this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;//如果我们是传入的选择器 则将选择器转换为dom对象
            // 把data中的成员 转换为getter和setter  注入到vue实例中 方便使用
            this._proxyData(this.$data)
            // 调用observer对象 监听数据变化
            new Observer(this.$data)
            // 调用compiler 解析指令和插值表达式 
            new Compiler(this)
        }
        _proxyData(data) {//vue传过来的参数 转换为getter和setter
            // 遍历data的所有属性
            Object.keys(data).forEach(key => {
                Object.defineProperty(this, key, {
                    // 可遍历 可枚举
                    enumerable: true,
                    configurable: true,
                    get() {
                        return data[key]
                    },
                    set(newValue) {
                        if (newValue === data[key]) {
                            return
                        } else {
                            data[key] = newValue;
                        }
                    }
                })
            })
            // 把data中的属性 注入到vue实例中

        }
    }
    class Observer {
        constructor(data) {
            this.walk(data)
        }
        // 1. 判断数据是否是对象，如果不是对象返回 
        // 2. 如果是对象，遍历对象的所有属性，设置为 getter/setter
        walk(data) {
            if (!data || typeof data != 'object') {
                return
            }
            Object.keys(data).forEach(key => {
                this.defineReactive(data, key, data[key])
            })
        }
        //  定义响应式成员  即对data总的数据实现setter和getter
        defineReactive(data, key, val) {
            //负责收集依赖 并发布通知
            let dep = new Dep()
            const that = this
            // 如果 val 是对象，继续设置它下面的成员为响应式数据 
            this.walk(val)
            Object.defineProperty(data, key, {
                enumerable: true,
                configurable: true,
                get() {
                    // 收集依赖
                    Dep.target && dep.addSub(Dep.target)
                    return val;
                },
                set(newValue) {
                    if (val === newValue) {
                        return
                    }
                    // 如果 newValue 是对象，设置 newValue 的成员为响应式 
                    that.walk(newValue)//这里不用this  因为在set方法中 在function的内部 会开启新的作用域 此时的this执行data对象   
                    val = newValue;
                    // 发布通知
                    dep.notify()
                }
            })
        }
    }
    class Compiler {
        constructor(vm) {
            this.el = vm.$el;
            this.vm = vm;
            this.compile(this.el);
        }
        // 编译模板 处理文本节点和元素节点
        compile(el) {
            const nodes = el.childNodes;
            Array.from(nodes).forEach(node => {
                if (this.isElementNode(node)) {
                    this.compileElement(node);
                } else if (this.isTextNode(node)) {
                    this.compileText(node)
                }
                // 如果当前节点中还有子节点，递归编译
                if (node.childNodes && node.childNodes.length) {
                    this.compile(node)
                }
            })

        }
        // 编译元素节点 处理指令
        compileElement(node) {
            // 暂时只涉及v-text和v-model
            //  逻辑思路 首先我们获取node节点所有的属性 查找到我们需要的指令 并用值替换
            // 下面我们打印一下node的所有属性 具体可切换google浏览器查看
            // console.log(node.attributes)

            // 遍历所有的属性节点
            Array.from(node.attributes).forEach(attr => {//Array.from 将伪数组转换为数组
                let attrName = attr.name;
                if (this.isDirective(attrName)) {
                    // 在指令中 有v-text  v-model等各种各样的指令 我们不能用if语句判断 如果用if 在后期 如果增加了别的指令 则不便于维护 需要手动增加if判断
                    // 因此 我们讲指令的v-去掉  只保留后面部分 即:v-text -> text  v-model ->model
                    attrName = attrName.substr(2)
                    let key = attr.value
                    this.update(node, key, attrName)
                }
            })
            // 判断是否是指令

        }
        // 通过update方法拼接处对应指令对应的方法 方便后序或者 如：v-text指令 则为textUpdater方法  v-model指令 则为modelUpdater方法 Updater字符串固定  因此 在后续 如果追加的不同的指令  执行增加指令名+Updater凭借的方法
        update(node, key, attrName) {
            let updateFun = this[attrName + 'Updater']
            // updateFun && updateFun(node, this.vm[key])
            //将这里使用call改变this 引入这里的updateFun是直接调用 this对象不是compiler 为了在modelUpdater和textUpdater方法中的this指向是compiler而不是window 所以使用call将updateFun的this指向改为compiler
            // 增加key参数 是为了在textUpdater和modelUpdater方法中能有key使用
            updateFun && updateFun.call(this, node, this.vm[key], key)
        }
        textUpdater(node, value, key) {
            node.textContent = value
            new Watcher(this.vm, key, (newValue) => {
                node.textContent = newValue;
            })
        }
        modelUpdater(node, value, key) {
            node.value = value
            new Watcher(this.vm, key, (newValue) => {
                node.value = newValue;
            })
            // 双向绑定
            node.addEventListener('input', () => {
                this.value[key] = node.value;
            })
        }
        // 编译文本节点 处理插值表达式
        compileText(node) {
            // console.dir(node)
            // {{ msg }}  解析： 插值表达式 是双括号中奖有一个变量 变量的前后可能有空格 可能无空格 可能有多个空格  我们需要将变量提出来 因此用正则表达式

            // 详解{{ msg }}案例正则使用规则
            // 1：正则的使用规范 首先用//表示正则的开始和介绍
            // 2：插值表达式中有{{}}，即前后两个大括号  因此这里的正则是 /{{}}/
            // 3: 犹豫{是特殊符号 因此需要转义符\ 故正则是 /\{\{\}\}/
            // 4: 匹配花括号 即{{}}中间的名字 而变量的名字前面都可能有空格 故：我们需要匹配任意的字符 使用点表示 即 '.' 点表示匹配任意的单个字符 ,不包括换行 后面跟上'+', 加号表示以前修饰的内容出现一次或者多次  因此 .+就可以匹配变量的名字 故 正则为: /\{\{.+\}\}/ 
            // 5: 我们在.+后面跟上问好'?' 表示今早的来结束匹配 故 正则为：/\{\{.+?\}\}/
            // 6: 通过/\{\{.+?\}\}/ 我们可以匹配到变量的名字 现在我们需要将匹配的变量名字提取出来 也就是把".+?"的这个位置的内容提取出来 在正则表达式中 这个非常简单 我们只需要需要提取内容的位置加上小括号(),小括号在正则中有分组的含义 我们可以获取到分组中的结果 最终的正则为/\{\{(.+?)\}\}/
            let reg = /\{\{(.+?)\}\}/
            let value = node.textContent;//获取文本的内容
            if (reg.test(value)) {
                let key = RegExp.$1.trim();//这里使用正则对象的构造函数 来获取第一个分组的内容 这里$1表示第一个分组的内容 如果要获取第二个 则用$2 获取之后 可能有空格 因此使trim去掉空格
                node.textContent = value.replace(reg, this.vm[key]);//replace() 方法用于在字符串中用一些字符替换另一些字符，或替换一个与正则表达式匹配的子串。
                new Watcher(this.vm, key, (newValue) => {
                    node.textContent = newValue;
                })
            }
        }
        // 判断元素是否是指令
        isDirective(attrName) {
            return attrName.startsWith('v-')
        }
        // 判断元素是否是元素节点
        isElementNode(node) {
            return node.nodeType === 1;
        }
        // 判断元素是否是文本节点
        isTextNode(node) {
            return node.nodeType === 3;
        }
    }
    // 要实现数据的响应机制 即数据变化 视图变化
    // 在vue的响应机制中 我们要使用观察模式来监听数据的变化 
    // 因此 在vue中我们要实现Dep和watcher  Dep的主要作用是收集依赖 在vue中的每一个响应属性 都会创建一个dep对象 负责手机依赖于该属性的所有依赖 即订阅者 并在数据更新时候发布通知  调用watcher对象中的update方法去更新视图 简单说明就是在数据劫持监听中的get去添加依赖 在set中去发布通知 

    class Dep {
        // 存储所有观察者
        constructor() {
            this.subs = []
        }
        // 添加观察者
        addSub(sub) {
            if (sub && sub.update) {
                this.subs.push(sub)
            }
        }
        // 发布通知
        notify() {
            this.subs.forEach(sub => {
                sub.update()
            })
        }
    }
    //数据变化 watcher去更新视图
    // 当我们去创建一个watcher对象时 需要把自己添加到自己的主题对象中去
    class Watcher {
        constructor(vm, key, cb) {
            this.vm = vm;
            // data中的属性名称
            this.key = key;
            // 回调函数 负责更新视图
            this.cb = cb;
            // 把watcher对象记录到Dep类的静态属性target中
            Dep.target = this;
            // 触发get方法 在get方法中调用addSub
            this.oldValue = vm[key]
            Dep.target = null;
        }
        // 当数据发生变化的时候 更新视图
        update() {
            let newValue = this.vm[this.key];
            if (newValue === this.oldValue) {
                return
            }
            this.cb(newValue)
        }
    }
    let vm = new Vue({
        el: '#app',
        data: {
            msg: 'hello',
            count: 123,
            person: {
                name: 'zs'
            }
        }
    })
    // vm.msg = { 'sex': 'ada' }



</script>